Skip to content

install: stop nested --bun from self-referencing its node shim - #30713

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/26b989d1/fix-nested-bun-node-shim-eloop
Jul 8, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
farm/26b989d1/fix-nested-bun-node-shim-eloop

Conversation

@robobun

@robobun robobun commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes #30711.

Repro (before)

$ cd $(mktemp -d) && echo '{"name":"x"}' > package.json && bun add vite
$ bun run --bun bun run --bun vite --version
/usr/bin/env: 'node': Too many levels of symbolic links
error: "vite" exited with code 126
error: "bun" exited with code 126
$ ls -la /tmp/bun-node-*/
lrwxrwxrwx ... bun  -> /tmp/bun-node-<sha>/bun    ← self-loop
lrwxrwxrwx ... node -> /tmp/bun-node-<sha>/bun    ← self-loop

Cause

create_fake_temporary_node_executable (src/install/lib.rs) writes
$BUN_NODE_DIR/{node,bun} as symlinks to the running bun, so scripts
that shebang /usr/bin/env node find one.

It was picking the symlink target in this order:

  1. argv[0] if it starts with /
  2. self_exe_path() otherwise

On a nested --bun, the OUTER bun prepends $BUN_NODE_DIR to PATH,
then execs the INNER bun — whose argv[0] becomes the PATH-resolved
$BUN_NODE_DIR/bun (the shim itself, both because
run_command.rs::run_binary_without_bunx_path passes the resolved
absolute path as argv0, and because bun's own shell rewrites
argv[0] to the resolved path before execve). The inner call then
uses that shim path as the link target, producing a self-loop.

Fix

Prefer bun_core::self_exe_path() — readlinks /proc/self/exe on
Linux, canonicalizes _NSGetExecutablePath on macOS — which always
resolves to the real bun regardless of how the process was invoked.
Fall back to an absolute argv[0] only if that fails. The result is
memoized via Once, so the cost is paid once per process.

Windows is unaffected — it already uses win::exe_path_w()
(the real image path) for the hardlinks.

Verification

Added test/cli/install/bun-run.test.ts ➜ nested --bun does not create a self-referencing node/bun shim. The test reproduces the
reporter's exact invocation (PATH-lookup of literal bun, shebang
script via node_modules/.bin/). Gate-checked:

  • without the fix: test fails with env: 'node': Too many levels of symbolic links and exit 126
  • with the fix: test passes, symlink resolves to the real bun exe

@robobun

robobun commented May 14, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:49 PM PT - Jun 23rd, 2026

❌ @robobun, your commit 27ef69c has 1 failures in Build #64339 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30713

That installs a local version of the PR into your bun-30713 executable, so you can run:

bun-30713 --bun

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3f11f839-c4e9-4ef7-9ad2-b313573c344c

📥 Commits

Reviewing files that changed from the base of the PR and between 11a2e2c and b9fbf68.

📒 Files selected for processing (2)
  • src/install/lib.rs
  • test/cli/install/bun-run.test.ts

Walkthrough

The PR fixes a symlink resolution bug in nested bun run --bun invocations by reordering how the temporary node/bun shim symlink targets are chosen, prioritizing the actual executable path from self_exe_path() over argv[0], and adds a regression test to validate the fix.

Changes

Nested --bun symlink resolution

Layer / File(s) Summary
Symlink target resolution logic
src/install/lib.rs
argv0_z symlink target selection is reordered to prefer optional_bun_path, then resolve the real executable via self_exe_path(), and only use absolute argv0 as a final fallback, preventing self-referential symlink chains in nested --bun scenarios.
Nested --bun symlink test
test/cli/install/bun-run.test.ts
New test case reproduces the nested bun run --bun vite scenario with node_modules/.bin shims, verifies no ELOOP errors occur, and confirms expected output and exit code 0.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing nested --bun invocations from creating self-referencing node/bun symlinks.
Description check ✅ Passed The description provides comprehensive context including repro steps, root cause analysis, fix explanation, and verification details; fully addresses the PR template sections.
Linked Issues check ✅ Passed The PR correctly implements the fix identified in #30711: prefers bun_core::self_exe_path() to obtain the real executable, falling back to absolute argv[0] only on failure, with test coverage confirming the nested --bun scenario works.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the nested --bun ELOOP issue: the library implementation change and a new test reproducing the exact bug scenario from #30711.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. Bun does not overwrite incorrect/dangling node-symlink, even when using bun --bun #4690 - Bun does not overwrite incorrect/dangling node-symlink; using self_exe_path() instead of argv[0] ensures the symlink always points to the real binary even if bun is moved
  2. using /tmp/bun-node for overriding node.js is incorrect #7504 - /tmp/bun-node symlink target based on argv[0] causes cross-install contamination; self_exe_path() resolves the actual executable path
  3. Several copies of Bun accidentally share each other's /private/tmp/bun-node-* #27722 - Multiple Bun copies share each other's /private/tmp/bun-node-* because argv[0] is used as symlink target; self_exe_path() keeps it accurate

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #4690
Fixes #7504
Fixes #27722

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused fix that prefers the canonical exe path over argv[0] for the node shim target, with a regression test that gates it.

Extended reasoning...

Overview

This PR reorders the symlink-target selection in create_fake_temporary_node_executable (src/install/lib.rs:602-647) so the /tmp/bun-node-<sha>/{node,bun} shims point at bun_core::self_exe_path() (the canonical resolved binary) rather than argv[0]. It adds a regression test in test/cli/install/bun-run.test.ts that reproduces the nested bun run --bun bun run --bun <bin> ELOOP failure from #30711.

Security risks

None introduced. The function already creates symlinks under a 0700-owned dir in shared /tmp; that hardening logic is untouched. If anything, deriving the link target from /proc/self/exe / _NSGetExecutablePath rather than caller-controlled argv[0] reduces the chance of the shim ever pointing somewhere unintended.

Level of scrutiny

Low–medium. The change is ~30 lines of control-flow reordering in a single function on the --bun slow path (marked #[cold]). I verified both call sites (run_command.rs:1969, PackageManager.rs:1200) pass an empty optional_bun_path, so the new first branch is never taken on entry and the effective change is simply "always try self_exe_path() before trusting absolute argv[0]". The out-param now feeds the canonical path into npm_execpath / load_node_js_config, which is strictly more correct. The new early-Ok(()) return when neither self_exe_path() nor an absolute argv[0] is available replaces the old behavior of symlinking to a relative argv[0], which was never useful.

Other factors

  • No CODEOWNERS coverage for either file.
  • Windows path is explicitly unaffected (already uses win::exe_path_w()).
  • The added test is gated correctly (skipIf(isWindows)), uses existing harness patterns, and asserts both the absence of the ELOOP message and exit 0.
  • The single CI failure (fetch-tcp-keepalive.test.ts on x64-asan) is a known unrelated flake per the retrigger commit.

@robobun
robobun force-pushed the farm/26b989d1/fix-nested-bun-node-shim-eloop branch from 8824ecb to 8a79a3a Compare June 5, 2026 09:36

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused fix that prefers self_exe_path() over argv[0] for the --bun node shim target, with a regression test reproducing the ELOOP.

Extended reasoning...

Overview

This PR fixes #30711 by reordering the symlink-target selection in create_fake_temporary_node_executable (src/install/lib.rs). Previously the function preferred an absolute argv[0] over self_exe_path(); on a nested bun run --bun bun run --bun ..., the inner process's argv[0] is the PATH-resolved <BUN_NODE_DIR>/bun shim itself, so the symlink pointed at itself and /usr/bin/env node failed with ELOOP. The fix prefers bun_core::self_exe_path() (which readlinks /proc/self/exe on Linux and canonicalizes _NSGetExecutablePath on macOS, memoized via Once), falling back to absolute argv[0] only if that fails. A regression test in test/cli/install/bun-run.test.ts reproduces the exact reporter scenario and gate-checks the fix.

Security risks

None. The change tightens correctness of the symlink target written under /tmp/bun-node-<sha>/. The existing 0700 / owner / mode checks on that directory are unchanged. Resolving to the real executable via /proc/self/exe is strictly safer than trusting argv[0], which is caller-controlled.

Level of scrutiny

Moderate. This is on the bun run --bun / lifecycle-script slow path (the function is #[cold]), not the plain bun foo.js startup path. The logic change is ~20 lines (mostly comments) and is a conservative reordering with graceful fallback: if self_exe_path() fails (e.g., no /proc), it falls back to the old absolute-argv[0] behavior; if neither is usable it returns early without modifying PATH rather than creating a broken relative symlink (an improvement over the old code). Both callers (run_command.rs and PackageManager.rs) pass an empty optional_bun_path, so the pre-supplied-path branch is unchanged in practice. Windows is unaffected (already uses win::exe_path_w()).

Other factors

  • The optional_bun_path out-param now carries the resolved exe path rather than argv[0], which feeds npm_execpath in run_command.rs — this matches what configure_env_for_run already does for npm_execpath, so it's a consistency improvement, not a regression.
  • The bug-hunting system found no issues; CodeRabbit had no actionable comments.
  • The regression test is well-constructed: it plants a bun symlink on PATH so the inner invocation's argv[0] is PATH-resolved, and uses a #!/usr/bin/env node shebang script in node_modules/.bin/ to surface ELOOP. The PR description confirms the test fails without the fix and passes with it.
  • No CODEOWNERS entries cover these paths.

@robobun

robobun commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased on main (conflict was comment-only in `create_fake_temporary_node_executable`, logic unchanged; re-verified the gate: the test fails on main's pre-fix `lib.rs` with the ELOOP error and passes with the fix).

CI build 60727: the only red job is `windows-x64-baseline-verify-baseline`, which failed in its setup step while unpacking Intel SDE (`sde.tar.xz` downloaded as 0 bytes → `7-Zip: Cannot open the file as archive`), before any test executed. That is a CI download flake, unrelated to this diff (the source change is `#[cfg(not(windows))]`, POSIX-only). A re-run of that one job should clear it.

@robobun

robobun commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased on main again. This time the conflict was purely additive in `test/cli/install/bun-run.test.ts`: #32644 appended a large-env-block test at the same spot as my nested-`--bun` regression test, so I kept both. `src/install/lib.rs` auto-merged cleanly and my fix is unchanged.

Re-verified the gate after rebase: the test fails on main's pre-fix `lib.rs` (ELOOP, exit 126) and passes with the fix. PR is mergeable.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — clean reorder to prefer self_exe_path() over argv[0], with a gate-verified regression test; the inline nit on the fallback arm is defense-in-depth for a near-unreachable case and not a blocker.

Extended reasoning...

Overview

This PR fixes #30711 by reordering the symlink-target selection in create_fake_temporary_node_executable (src/install/lib.rs, POSIX-only branch). Previously the function preferred an absolute argv[0] over self_exe_path(); on nested bun run --bun bun run --bun <bin>, the inner bun's argv[0] is the shim path itself, producing a self-referencing symlink and ELOOP. The fix prefers the caller-supplied optional_bun_path, then bun_core::self_exe_path() (kernel-provided real image path, memoized via Once), and only falls back to absolute argv[0] if that fails. A regression test in test/cli/install/bun-run.test.ts reproduces the exact reporter scenario and was gate-verified (fails on pre-fix lib.rs with ELOOP/exit 126, passes with the fix). Two files changed, ~40 effective lines (mostly explanatory comments).

Security risks

None introduced. The symlink directory creation/ownership checks (mkdir 0700, lstat uid/mode guard) are unchanged. Switching the link target from caller-controlled argv[0] to the kernel-reported executable path (/proc/self/exe / _NSGetExecutablePath) is strictly safer — it removes a way for a parent process to influence what the shim points at. Windows is untouched (already uses win::exe_path_w()).

Level of scrutiny

Low–moderate. This is a small, well-scoped reorder of two existing data sources inside a single #[cfg(not(windows))] block, with no new syscalls, no new unsafe, and no change to the EEXIST/readlink/replace loop or directory hardening. The accompanying test is precise and was verified to fail without the fix. No CODEOWNERS cover these paths.

Other factors

The one inline finding is explicitly a nit: the absolute-argv[0] fallback can still self-loop, but only when self_exe_path() fails — i.e. /proc not mounted on Linux or _NSGetExecutablePath failure on macOS, both treated as effectively unreachable elsewhere in the codebase (util.rs uses .expect("unreachable") on the same call). The PR is a strict improvement on every realistic path, so this is defense-in-depth the author may optionally fold in, not a merge blocker. CI history shows only unrelated infra flakes (Windows SDE download, fetch-tcp-keepalive ASAN); the PR has been rebased twice with the gate re-verified each time.

Comment thread src/install/lib.rs
Nested `bun run --bun bun run --bun <script>` produced a
self-referencing symlink at `$BUN_NODE_DIR/{node,bun}` → itself, so the
next `/usr/bin/env node` bailed out with "Too many levels of symbolic
links" (exit 126).

When the OUTER bun prepends `$BUN_NODE_DIR` to PATH and execs the
INNER bun, the inner's argv[0] becomes the PATH-resolved
`$BUN_NODE_DIR/bun` — the shim the inner call is about to (re)write.
`create_fake_temporary_node_executable` was preferring that argv[0]
over the real executable path, so it wrote the self-loop.

Flip the preference: ask the OS for the absolute path first
(`self_exe_path()` readlinks `/proc/self/exe` on Linux, canonicalizes
`_NSGetExecutablePath` on macOS — both return the real bun regardless
of how it was invoked). Fall back to an absolute argv[0] only if that
fails. Windows already uses the real image path and is unaffected.

Fixes #30711
@robobun
robobun force-pushed the farm/26b989d1/fix-nested-bun-node-shim-eloop branch from e5c1d20 to 27ef69c Compare June 23, 2026 23:37
@robobun

robobun commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. I folded in the fallback nit: when `self_exe_path()` fails and `argv[0]` is the shim under `BUN_NODE_DIR` (nested `--bun` with no `/proc`), the fallback now returns `Ok(())` (the outer bun already planted working shims + PATH) instead of rewriting the link into a self-loop. Absolute argv[0] outside `BUN_NODE_DIR` (shebang launch) is still used; a non-absolute one still propagates the OS error.

Also squashed the branch into a single commit (dropped the empty ci:retrigger) and re-verified the gate on the rebased tree: the test fails on main's pre-fix `lib.rs` with ELOOP/exit 126 and passes with the fix. Commit 27ef69c.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the fallback-arm nit is folded in and the gate-checked regression test covers the exact repro.

Extended reasoning...

Overview

This PR fixes #30711: nested bun run --bun bun run --bun <bin> invocations created a self-referencing symlink at $BUN_NODE_DIR/{bun,node}, causing /usr/bin/env node to fail with ELOOP. The fix reorders symlink-target selection in create_fake_temporary_node_executable (src/install/lib.rs) to prefer bun_core::self_exe_path() — the canonical, OS-resolved executable path — over argv[0], which on nested --bun is the shim path itself. A regression test in test/cli/install/bun-run.test.ts reproduces the reporter's exact invocation and has been gate-checked (fails on pre-fix lib.rs with ELOOP/exit 126, passes with the fix).

Since my previous review, the author folded in my nit: the fallback arm (taken only when self_exe_path() fails) now explicitly checks argv0.starts_with(BUN_NODE_DIR) and returns Ok(()) early rather than rewriting the outer bun's working shim into a self-loop. The inline thread is resolved and the change matches what I suggested.

Security risks

None introduced. The /tmp/bun-node-<sha> directory ownership and 0700-permission checks are untouched. If anything, preferring the OS-resolved canonical path over caller-controlled argv[0] as the symlink target is strictly safer — argv[0] is attacker-influenceable, /proc/self/exe is not.

Level of scrutiny

Moderate. This is a focused ~30-line logic reorder in one POSIX-only function, with no new syscalls or trust boundaries. The Windows path is unchanged (already uses win::exe_path_w()). The change is well-commented, the root cause is clearly documented inline with the issue number, and the test is a faithful repro of the user-reported bug.

Other factors

  • No CODEOWNERS coverage for either file.
  • No outstanding human-reviewer comments; coderabbit had no findings; my own prior nit is resolved.
  • The author re-verified the fail/pass gate after each rebase (three times), and the test follows the existing patterns in bun-run.test.ts (tempDir, Bun.spawn, describe.concurrent, it.skipIf(isWindows)).
  • One incidental behavior change: when argv[0] is an absolute non-shim path (e.g. shebang launch), the symlink now points at the canonical exe rather than argv[0] verbatim. This is the intended improvement and the linked-issues bot notes it likely fixes #4690/#7504/#27722 as well.

@robobun

robobun commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

CI build 64339 (commit 27ef69c): the one red job is `test/js/web/streams/streams-leak.test.ts` on `debian 13 x64 - test-bun`, which the build also flags with a `flaky` annotation. That is an RSS-threshold streams memory-leak test, unrelated to this diff (which only touches `src/install/lib.rs` and `test/cli/install/bun-run.test.ts`, neither streams-related). The install/node-shim change here cannot affect it. Diff is green on every other lane and the PR is mergeable; a re-run of that one job should clear it.

@Jarred-Sumner
Jarred-Sumner merged commit d996ec7 into main Jul 8, 2026
77 of 78 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/26b989d1/fix-nested-bun-node-shim-eloop branch July 8, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Too many levels of symbolic links in nested usages of --bun

2 participants